package com.nulldev.util.web.HttpServer3.rewrite;

import static com.nulldev.util.web.HttpServer3.rewrite.InternalImpl.ENFORCE_DATA_LIMIT_SIZE;
import static com.nulldev.util.web.HttpServer3.rewrite.InternalImpl.__get_port_http;
import static com.nulldev.util.web.HttpServer3.rewrite.InternalImpl.__get_port_https;
import static com.nulldev.util.web.HttpServer3.rewrite.InternalImpl.readTillNextLine;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.ALPN_HTTP_1_ONLY;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.HARDCODED_ERROR;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.HARDCODED_ICE_ERROR;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.SO_LINGER_VAL;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.SO_TIMEOUT_VAL;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__apply_settings_to_server_socket;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__get_pw;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.__sock_SO_SNDBUF;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.generateStandardSocket;
import static com.nulldev.util.web.HttpServer3.util.HttpServerUtil.getTLSVersion;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.LongAdder;
import java.util.regex.Pattern;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.asyncTasks.AsyncTaskCallback;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Returnable;
import com.nulldev.util.concurrency.threadIt.v4.executors.bqde.BQDE;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.maps.FastHashMap;
import com.nulldev.util.data.Arrays.maps.trie.TrieMap;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.file.FileUtil;
import com.nulldev.util.io.legacy.MimeType;
import com.nulldev.util.io.throttling.BandwidthThrottledOutputStream;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.Version;
import com.nulldev.util.networking.ip.IP;
import com.nulldev.util.networking.misc.tls.TLSStandardFiltering;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpServer3.CopyAcceleration;
import com.nulldev.util.web.HttpServer3.HttpServer;
import com.nulldev.util.web.HttpServer3.HttpServer3Flags;
import com.nulldev.util.web.HttpServer3.HttpServer3Settings;
import com.nulldev.util.web.HttpServer3.HttpServerErrorStates;
import com.nulldev.util.web.HttpServer3.MicroCache;
import com.nulldev.util.web.HttpServer3.MicroCache.CachedPage;
import com.nulldev.util.web.HttpServer3.Page;
import com.nulldev.util.web.HttpServer3.Response;
import com.nulldev.util.web.HttpServer3.SSLBehaviour;
import com.nulldev.util.web.HttpServer3.extensions.envStats.TCPDebugPage;
import com.nulldev.util.web.HttpServer3.extensions.fakeFlare.CDNCGITracePage;
import com.nulldev.util.web.HttpServer3.extensions.nCaptcha.nCaptcha;
import com.nulldev.util.web.HttpServer3.extensions.sse.SSEPage;
import com.nulldev.util.web.HttpServer3.extensions.websocket.IWebSocketPage;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.HttpPage;
import com.nulldev.util.web.HttpServer3.legacyPages.v2.HttpPage2;
import com.nulldev.util.web.HttpServer3.pages.v3.ASyncHttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3.CachingMethod;
import com.nulldev.util.web.HttpServer3.pages.v3.HttpPage3.EnhancedSecurityChecks;
import com.nulldev.util.web.HttpServer3.util.HTTPPrintWriter;
import com.nulldev.util.web.HttpServer3.util.SectionedInputStream;

/**
 * A continuation and improvement of HttpServer 2. <br>
 * 
 * HttpServer, a lightweight and fast web server that is simple to use and
 * quick.
 * 
 * New Features: <br>
 * <ul>
 * <li>Body data is now accquired correctly. (FIXED)</li>
 * <li>Internal Anti DDOS/DOS system. (rate-limiter) [work-in-progress]</li>
 * <li>Micro caching (>8MB on systems above 128MB) [done]</li>
 * <li>Improved HTTPS support [done]</li>
 * <li>(POSSIBLE) HTTP/2.0 support</li>
 * <li>iWebKit's STTP support (Secure [Hyper] Text Transfer Protocol) [wip]</li>
 * </ul>
 * 
 * API Changes: <br>
 * <ul>
 * <li>Removed overrideResponse. (known as "override" in 2.x)</li>
 * <li>Constructor has been changed to accept KeyManagers for SSL support.</li>
 * <li>The flags and settings object is still incomplete.</li>
 * <li>Removed most iWebKit references.</li>
 * <li>Fixed HttpPage2's re-implementation.</li>
 * </ul>
 * 
 * @author null
 * @since 3.0.0 (1.0 was implemented in 2.5, 2.0 in 2.6, 2.0.1 in 2.6.2, 2.1 in
 *        2.7)
 */
public class HttpServer3RW implements HttpServer {
	/**
	 * Ongoing work: <br>
	 * - Timeout and dead request fixes. (PHASE 2) <br>
	 * - Move getTLSVersion(), generateSSL(KeyManager[]),
	 * generateSSLSocket(KeyManager[]) and generateStandardSocket() into a separate
	 * utility class.
	 *
	 * See the CHANGELOG file for changes.
	 *
	 * Planned work: <br>
	 * - Reattempt HTTP/2.0. <br>
	 * - Merge libSocketReader patchset, once SSL is compliant via NIO. <br>
	 * - Update the timingDebug to include basic URL indicators.
	 */
	private static final Logger log = LoggerFactory.getLoggerDT(Arguments.hasArgument("--HttpServer3.timingDebug"));

	/* */

	public static final Version VERSION = new Version(3, 0, 6);

	public static final long UNLIMITED_CONNECTIONS = -1;

	// END

	public boolean isTimingDebugEnabled = Arguments.hasArgument("--HttpServer3.timingDebug");

	final HttpServer3Settings settings = new HttpServer3Settings();
	final HttpServer3Flags flags = new HttpServer3Flags();

	final IExecutorManager td = Arguments.hasArgument("--HttpServer3.useGlobalExecutor") ? ExecutorManager.get()
			: new ExecutorManager(new BQDE(Runtime.getRuntime().availableProcessors() * 24, 4));

	private final Map<String, Page> PAGES = (this.flags.use_trie_map ? new TrieMap<String, Page>() : new FastHashMap<String, Page>(true));
	private final Map<String, String> ERROR_PAGES = (this.flags.use_trie_map ? new TrieMap<String, String>() : new FastHashMap<String, String>(true));

	private long connectionsLimit = UNLIMITED_CONNECTIONS;

	private final LongAdder connections = new LongAdder();

	private final MicroCache cache;

	private ServerSocket httpServerSocket;
	SSLServerSocket httpsServerSocket;
	private SSLSocketFactory fact;
	private SSLParameters default_params;

	private SSLServerSocket generateSSLSocket(final KeyManager[] km) throws Exception {
		final SSLContext context = SSLContext.getInstance(getTLSVersion());
		final TrustManager[] trustAllCerts = new TrustManager[]
			{ new X509TrustManager() {
				@Override
				public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
				}

				@Override
				public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
				}

				@Override
				public java.security.cert.X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			} };
		context.init(km, trustAllCerts, null);
		final SSLParameters params = context.getDefaultSSLParameters();
		params.setEndpointIdentificationAlgorithm("HTTPS");
		params.setUseCipherSuitesOrder(true);
		params.setProtocols(TLSStandardFiltering.filterProtocols(params.getProtocols()));
		params.setCipherSuites(TLSStandardFiltering.filterCiphers(params.getCipherSuites()));
		if (JVM.getFullVersion().newerThan(1, 8, 251))
			params.setApplicationProtocols(ALPN_HTTP_1_ONLY);
		this.default_params = params;
		final SSLServerSocketFactory ssl = (SSLServerSocketFactory) context.getServerSocketFactory();
		final SSLSocketFactory ssf = context.getSocketFactory();
		this.fact = ssf;
		final SSLServerSocket socket = (SSLServerSocket) ssl.createServerSocket();
		socket.setSSLParameters(params);
		try {
			__apply_settings_to_server_socket(socket, this);
		} catch (SocketException ex) {
			log.error("Failed to apply server socket settings!", ex);
		}
		return socket;
	}

	private InetAddress addr;
	int port;
	int httpsPort = 443;

	private final Http1Component h1c = new Http1Component(this);
	final Http2Component h2c = new Http2Component(this);

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param localHost   (set to true if you want to test the server)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3RW(final int defaultPort, final boolean localHost, final SSLBehaviour ssl, final KeyManager[] km) throws Exception {
		this(defaultPort, localHost, ssl, km, MicroCache.DEFAULT_SIZE);
	}

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param localHost   (set to true if you want to test the server)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3RW(final int defaultPort, final boolean localHost, final SSLBehaviour ssl, final KeyManager[] km, final int cacheSize) throws Exception {
		this(defaultPort,
				(localHost ? IP.localhost()
						: (Arguments.hasArgument("--HttpServer3.bindToAll", "--NTubeServer.bindToAll") ? IP.allAddresses() : IP.lanAddress())),
				ssl, km, cacheSize);
	}

	/**
	 * HttpServer3 constructor.
	 * 
	 * @param defaultPort (default port for HTTP then +- for HTTPS)
	 * @param addr        (IP address to bind to)
	 * @param ssl         (enables separate SSL port, requires a KeyManager)
	 * @param km          (can be <code>null</code> if HTTPS/SSL is not needed)
	 * @throws Exception
	 */
	public HttpServer3RW(final int defaultPort, final InetAddress addr, final SSLBehaviour ssl, final KeyManager[] km, final int cacheSize) throws Exception {
		this.cache = new MicroCache(cacheSize);
		this.addr = Variables.requireNonNullObject(addr, "addr");
		this.port = __get_port_http(ssl, defaultPort);
		this.httpsPort = __get_port_https(ssl, defaultPort);
		if (Arguments.hasArgument("--HttpServer3.enableCaptcha")) {
			log.info("Enabled captcha.");
			nCaptcha.loadCaptchaSystem(this);
			this.settings.enableNCaptcha = true;
			/* Required for v4. */
			this.flags.enable_ws = true;
		}
		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					if (HttpServer3RW.this.port == -1)
						return;
					HttpServer3RW.this.httpServerSocket = generateStandardSocket();
					try {
						__apply_settings_to_server_socket(HttpServer3RW.this.httpServerSocket, HttpServer3RW.this);
					} catch (Exception ex) {
						log.error("Failed to apply server socket settings!", ex);
					}
					HttpServer3RW.this.httpServerSocket.bind(new InetSocketAddress(HttpServer3RW.this.addr, HttpServer3RW.this.port), 1024);
					log.info("Server is now exposed. [ip=" + HttpServer3RW.this.addr + ",port=" + HttpServer3RW.this.port + ",ssl=" + ssl + "]");
					while (true) {
						try {
							final Socket s = HttpServer3RW.this.httpServerSocket.accept();
							// TODO: Should there be a threshold at which we completely reject new
							// connections? (as in not even open and check if we can squeeze a user in)
							HttpServer3RW.this.td.tryAutoAsyncSubmit(HttpServer3RW.this.flags.use_async_submissions, () -> {
								try {
									if (!s.isClosed()) {
										try {
											s.setTcpNoDelay(!HttpServer3RW.this.flags.force_disable_nodelay);
											if (HttpServer3RW.this.flags.tcp_keep_alive) {
												s.setKeepAlive(true);
											}
											if (HttpServer3RW.this.settings.enableAddressReuse) {
												s.setReuseAddress(true);
											}
											if (HttpServer3RW.this.settings.enableSoLinger) {
												s.setSoLinger(true, SO_LINGER_VAL);
											} else {
												s.setSoLinger(false, -1);
											}
											if (HttpServer3RW.this.settings.enableTimeoutEnforcement) {
												s.setSoTimeout(SO_TIMEOUT_VAL);
											}
											if (HttpServer3RW.this.flags.sockets_set_throughput) {
												s.setTrafficClass(0x08);
											}
										} catch (Exception e) {
											log.dwarn("Failed to apply socket settings! [e=" + e.getMessage() + "]");
										}
									}
									HttpServer3RW.this.initialHandle(s, false);
								} catch (Exception e) {
									if (!(e instanceof SSLHandshakeException || e instanceof SSLException)
											&& !(e.getMessage() != null && e.getMessage().equalsIgnoreCase("Connection reset"))) {
										log.error("Handle failed!", e);
										IOUtils.closeQuietly(s);
										return s;
									}
								}
								return s;
							}).addCallbackTask(new AsyncTaskCallback<Socket>() {

								@Override
								public void onCompletion(final Socket result) {
									HttpServer3RW.this.connections.decrement();
								}

								@Override
								public void onError(Throwable e) {
									HttpServer3RW.this.connections.decrement();
									if (e instanceof SocketException && (e.getMessage().contains("Broken pipe (Write failed)")
											|| e.getMessage().equalsIgnoreCase("Socket Closed") || e.getMessage().equals("Socket is closed"))) {
									} else if (e instanceof SSLHandshakeException) {
										if (e.getMessage().endsWith("certificate_unknown") || e.getMessage().endsWith("Broken pipe (Write failed)")) {
											// Literally no-one cares.
										} else {
											log.error("SSL: Error reached!", e.getMessage());
										}
									} else {
										log.error(e);
									}
									IOUtils.closeQuietly(s);
								}

								@Override
								public void onSubmissionRejection() {
									/* We're too busy doing things, abandon ship! */
									IOUtils.closeQuietly(s);
								}
							});
							HttpServer3RW.this.connections.increment();
						} catch (Exception e) {
							if (!(e instanceof SocketTimeoutException))
								log.error(e);
						}
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}, "HS3-HTTP").start();
		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					if (HttpServer3RW.this.httpsPort == -1)
						return;
					if (km != null) {
						HttpServer3RW.this.httpsServerSocket = HttpServer3RW.this.generateSSLSocket(km);
					}
					if (HttpServer3RW.this.httpsServerSocket != null) {
						HttpServer3RW.this.httpsServerSocket.bind(new InetSocketAddress(HttpServer3RW.this.addr, HttpServer3RW.this.httpsPort), 1024);
						log.info("HTTPS Server is now exposed. [ip=" + HttpServer3RW.this.addr + ",port=" + HttpServer3RW.this.httpsPort + ",ssl=" + ssl + "]");
					}
					if (HttpServer3RW.this.httpsServerSocket != null) {
						while (true) {
							try {
								final Socket s = HttpServer3RW.this.httpsServerSocket.accept();
								// TODO: Should there be a threshold at which we completely reject new
								// connections? (as in not even open and check if we can squeeze a user in)
								HttpServer3RW.this.td.tryAutoAsyncSubmit(HttpServer3RW.this.flags.use_async_submissions, () -> {
									try {
										if (!s.isClosed()) {
											try {
												s.setTcpNoDelay(!HttpServer3RW.this.flags.force_disable_nodelay);
												if (HttpServer3RW.this.flags.tcp_keep_alive) {
													s.setKeepAlive(true);
												}
												if (HttpServer3RW.this.settings.enableAddressReuse) {
													s.setReuseAddress(true);
												}
												if (HttpServer3RW.this.settings.enableSoLinger) {
													s.setSoLinger(true, SO_LINGER_VAL);
												} else {
													s.setSoLinger(false, -1);
												}
												if (HttpServer3RW.this.settings.enableTimeoutEnforcement) {
													s.setSoTimeout(SO_TIMEOUT_VAL);
												}
												if (HttpServer3RW.this.flags.sockets_set_throughput) {
													s.setTrafficClass(0x08);
												}
											} catch (Exception e) {
												log.dwarn("Failed to apply socket settings! [e=" + e.getMessage() + "]");
											}
										}
										HttpServer3RW.this.initialHandle(s, true);
									} catch (Exception e) {
										if (!(e instanceof SSLHandshakeException || e instanceof SSLException)) {
											log.error("Handle failed!", e);
											IOUtils.closeQuietly(s);
											return s;
										}
									}
									return s;
								}).addCallbackTask(new AsyncTaskCallback<Socket>() {

									@Override
									public void onCompletion(Socket result) {
										HttpServer3RW.this.connections.decrement();
									}

									@Override
									public void onError(Throwable e) {
										HttpServer3RW.this.connections.decrement();
										if (e instanceof SocketException && (e.getMessage().contains("Broken pipe (Write failed)")
												|| e.getMessage().equalsIgnoreCase("Socket Closed") || e.getMessage().equals("Socket is closed"))) {
										} else if (e instanceof SSLHandshakeException) {
											if (e.getMessage().endsWith("certificate_unknown") || e.getMessage().endsWith("Broken pipe (Write failed)")) {
												// Literally no-one cares.
											} else {
												log.error("SSL: Error reached!", e.getMessage());
											}
										} else {
											log.error(e);
										}
										IOUtils.closeQuietly(s);
									}

									@Override
									public void onSubmissionRejection() {
										/* We're too busy doing things, abandon ship! */
										IOUtils.closeQuietly(s);
									}
								});
								HttpServer3RW.this.connections.increment();
							} catch (Exception e) {
								if (!(e instanceof SocketTimeoutException))
									log.error(e);
							}
						}
					}
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}, "HS3-HTTPS").start();
		this.ERROR_PAGES.put("PAGE_NOT_FOUND", HttpServerErrorStates.PAGE_NOT_FOUND);
		this.ERROR_PAGES.put("INTERNAL_ERROR", HttpServerErrorStates.INTERNAL_ERROR);
		this.ERROR_PAGES.put("SERVER_BUSY", HttpServerErrorStates.SERVER_BUSY);
		this.ERROR_PAGES.put("SECURITY_CHECK_FAIL", HttpServerErrorStates.SECURITY_CHECK_FAIL);
		this.ERROR_PAGES.put("INVALID_REQUEST", HttpServerErrorStates.INVALID_REQUEST);
		this.ERROR_PAGES.put("SERVICE_UNAVAILABLE", HttpServerErrorStates.SERVICE_UNAVAILABLE);
		this.ERROR_PAGES.put("BAD_RESPONSE", HttpServerErrorStates.BAD_RESPONSE);
	}

	public void setServerName(final String value) {
		this.settings.setServerName(value);
	}

	public Page getPage(final String id, final Object sock) {
		if (id == null)
			return null;
		final Page initial = this.PAGES.get(id);
		return (initial != null ? initial : unknown(id, sock));
	}

	public Page unknown(final String id, final Object sock) {
		return null;
	}

	public String getPageNotFound() {
		return this.ERROR_PAGES.get("PAGE_NOT_FOUND");
	}

	public void setPageNotFound(final String pnf) {
		if (pnf == null || pnf.isEmpty()) {
			this.ERROR_PAGES.put("PAGE_NOT_FOUND", HttpServerErrorStates.PAGE_NOT_FOUND);
		} else {
			this.ERROR_PAGES.put("PAGE_NOT_FOUND", pnf);
		}
	}

	public String getInternalErrorPage() {
		return this.ERROR_PAGES.get("INTERNAL_ERROR");
	}

	public void setInternalErrorPage(final String iep) {
		if (iep == null || iep.isEmpty()) {
			this.ERROR_PAGES.put("INTERNAL_ERROR", HttpServerErrorStates.INTERNAL_ERROR);
		} else {
			this.ERROR_PAGES.put("INTERNAL_ERROR", iep);
		}
	}

	public InetAddress getAddress() {
		return this.addr;
	}

	public int getPort() {
		return this.port;
	}

	public int getHTTPSPort() {
		return this.httpsPort;
	}

	private static final String[] REGULAR_REQUEST_ENTRIES = new String[]
		{ "GET ", "POST", "OPTI", "PUT ", "DELE", "HEAD", "TRAC", "PATC", "CONN", "keep", "PRI " };

	private static boolean isRegularRequest(final String p) {
		return ArrayUtils.StringArrayUtils.containsStartsWith(REGULAR_REQUEST_ENTRIES, p);
	}

	private boolean isTooManyConnections() {
		if (!this.settings.enableNoDDOS)
			return false;
		else if (this.connectionsLimit <= 0)
			return false;
		else
			return this.connections.sum() >= (this.settings.enableNoDDOS ? NO_DDOS_CONNECTION_LIMIT : this.connectionsLimit);
	}

	private void __TIMING(final String str, final long stamp) {
		// XXX: Roll this out to the rest of HttpServer3.
		if (this.isTimingDebugEnabled)
			log.debug(str + " " + (System.currentTimeMillis() - stamp) + " ms");
	}

	private static final int NO_DDOS_CONNECTION_LIMIT = 4096; // 10240
	private static final int REGULAR_REQUEST_DETECTION_SIZE = 4;

	private void initialHandle(final Socket socket, boolean expectHTTPS) throws Exception {
		final long start = this.isTimingDebugEnabled ? System.currentTimeMillis() : 0;
		__TIMING("actualhandle::start:", start);
		if (this.isTooManyConnections()) {
			if (this.connections.sum() + 32 >= (this.settings.enableNoDDOS ? NO_DDOS_CONNECTION_LIMIT : this.connectionsLimit) || expectHTTPS) {
				// Too many connections, just refuse the user.
				// Using TLS/SSL at this point would be too much extra load, bail instead.
				IOUtils.closeQuietly(socket);
				return;
			} else {
				log.debug("[invoke_error] Cause: CONNECTION_LIMIT_REACHED_SAFE");
				this._invoke_error(null, __get_pw(socket.getOutputStream(), __sock_SO_SNDBUF(socket)), socket, ErrorType.ERROR_BUSY);
				return;
			}
		} else {
			__TIMING("actualhandle::conn_check:", start);
			final InputStream _is = socket.getInputStream();
			final SectionedInputStream si = new SectionedInputStream(_is, REGULAR_REQUEST_DETECTION_SIZE);
			final byte[] initData = new byte[REGULAR_REQUEST_DETECTION_SIZE];
			if (si.read(initData) != 4) {
				log.error("[initialHandle] REGULAR_REQUEST_DETECTION failed! Bailing out...");
				IOUtils.closeAllQuietly(_is, si, socket);
				return;
			}
			IOUtils.closeQuietly(si);
			final InputStream rawInputStream = new SequenceInputStream(si, _is);
			__TIMING("actualhandle::is->initdata:", start);
			final String id = new String(initData, CharsetUtil.US_ASCII);
			final boolean requiresHTTPS = !isRegularRequest(id);
			SSLSocket ssl = null;
			InputStream inputStream;
			if (requiresHTTPS && this.fact != null) {
				ssl = (SSLSocket) this.fact.createSocket(socket, rawInputStream, true);
				ssl.setSSLParameters(this.default_params);
				inputStream = ssl.getInputStream();
				expectHTTPS = true;
				__TIMING("actualhandle::__fact_generate_ssl:", start);
			} else if (requiresHTTPS && this.fact == null) {
				log.warn("Socket [" + socket + "] requested(?) HTTPS...we don't have HTTPS, bailing.");
				IOUtils.closeAllQuietly(rawInputStream, socket);
				return;
			} else {
				inputStream = rawInputStream;
			}
			OutputStream os = ssl != null ? ssl.getOutputStream() : socket.getOutputStream();
			final int prefOutSize = __sock_SO_SNDBUF(socket);
			if (this.settings.bandwidthThrottler != null)
				os = new BandwidthThrottledOutputStream(os, this.settings.bandwidthThrottler);
			final HTTPPrintWriter dos = __get_pw(os, false, prefOutSize);
			__TIMING("actualhandle::__get_pw:", start);
			this.handle((ssl != null ? ssl : socket), inputStream, os, dos, expectHTTPS, start);
		}
	}

	public static final int getDataLimitSize() {
		return ENFORCE_DATA_LIMIT_SIZE;
	}

	public enum ErrorType { // FIXME: We should be protected once NX is merged
		ERROR_400,
		ERROR_404,
		ERROR_500,
		ERROR_503,
		ERROR_BUSY,
		ERROR_BAD_RESPONSE,
		ERROR_SECURITY_CHECK
	}

	protected void _invoke_error(final InputStream is, final HTTPPrintWriter out, final Socket s, final ErrorType et) throws Exception {
		String data;
		if (this.ERROR_PAGES.isEmpty()) {
			data = HARDCODED_ERROR;
			out.println("HTTP/1.1 503 Internal Error Occured");
		} else {
			switch (et) {
				case ERROR_400: {
					data = this.ERROR_PAGES.get("INVALID_REQUEST");
					out.println("HTTP/1.1 400 Bad Request");
					break;
				}
				case ERROR_404:
					data = this.ERROR_PAGES.get("PAGE_NOT_FOUND");
					out.println("HTTP/1.1 404 Page Not Found");
					break;
				case ERROR_500:
					data = this.ERROR_PAGES.get("INTERNAL_ERROR");
					out.println("HTTP/1.1 500 Internal Error Occured");
					break;
				case ERROR_503:
					data = this.ERROR_PAGES.get("SERVICE_UNAVAILABLE");
					out.println("HTTP/1.1 503 Service Unavailable");
					break;
				case ERROR_BAD_RESPONSE:
					data = this.ERROR_PAGES.get("BAD_RESPONSE");
					out.println("HTTP/1.1 500 Internal Error Occured");
					break;
				case ERROR_BUSY:
					data = this.ERROR_PAGES.get("SERVER_BUSY");
					out.println("HTTP/1.1 509 Bandwidth Limit Exceeded");
					break;
				case ERROR_SECURITY_CHECK:
					data = this.ERROR_PAGES.get("SECURITY_CHECK_FAIL");
					out.println("HTTP/1.1 400 Bad Request");
					break;
				default:
					throw new NullPointerException("Unknown error type: " + et);
			}
		}
		if (StringsUtil.isBlank(data))
			data = HARDCODED_ICE_ERROR;
// TODO: Replace below with the StringsUtil version.
//		StringsUtil.replace(data, "$HTTPServer.WebServerName", this.settings.getServerName());
		data = data.replaceAll(Pattern.quote("$HTTPServer.WebServerName"), this.settings.getServerName());
		out.println("Content-type: text/html");
		out.println("Server-name: " + this.settings.getServerName());
		out.println("Server: HttpServer3 from nullUtil 3.0");
		out.println("Content-Length: " + data.getBytes().length);
		out.println("Connection: close");
		out.println("");
		out.print(data);
		out.flush();
		IOUtils.closeAllQuietly(is, out, s);
	}

	protected boolean isValid(final Response resp) {
		if (resp == null) {
			log.debug("[isValid] ERROR_REASON: RESPONSE_WAS_NULL");
			return false;
		}
		if (resp.getResponseCode() <= 100 || resp.getResponseCode() >= 600) {
			log.debug("[isValid] ERROR_REASON: INVALID_RESPONSE_CODE [code: " + resp.getResponseCode() + "]");
			return false;
		}
/// XXX: Shouldn't we be checking for a lack of NIO-copyable?
		if (resp.getInputStream() == null && !resp.copyable()) {
			log.debug("[isValid] ERROR_REASON: NO_RESPONSE_STREAM");
			return false;
		}
		return true;
	}

	final CopyAcceleration ccm = CopyAcceleration.get();

	/* */

	private void handle(final Socket socket, final InputStream is, final OutputStream os, final HTTPPrintWriter out, final boolean expectHTTPS,
			final long start) throws Exception {
		final variable<Boolean> __keep_alive = new variable<Boolean>(false);
		do {
			if (socket.isClosed()) {
				log.error("handle(): Uhh, the socket is closed, bailing out of this request...");
				IOUtils.closeAllQuietly(socket, is, out);
				__keep_alive.set(false);
				break;
			}
			final String request0 = readTillNextLine(is);
			__TIMING("handle::readRequest0:", start);
			if (request0 == null || request0.isEmpty()) {
				IOUtils.closeAllQuietly(out, is, socket);
				__keep_alive.set(false);
				break;
			} else if (request0.equals("PRI * HTTP/2.0") && this.flags.enable_http2) {
				log.debug("actualHandle::HTTP2: Creating SESSION");
				h2c.__pri_http2(os, is, socket);
				break;
			} else if (request0.endsWith("STTP/1.0")) {
				/* Removed in 3.0.6 */
				IOUtils.closeAllQuietly(out, is, socket);
				__keep_alive.set(false);
				break;
			} else {
				if (h1c.handleHTTP(request0, socket, is, os, out, start, expectHTTPS, __keep_alive)) {
					break;
				}
			}
		} while (__keep_alive.object() && !socket.isClosed() && !socket.isInputShutdown());
		IOUtils.closeAllQuietly(is, out, socket);
	}

	/**
	 * Allows a page to be processed then returning a Page from a separate thread.
	 * 
	 * @since 3.0
	 * @param page - Page
	 */
	public void deferredAddPage(final Page page) {
		td.asyncSubmit(() -> {
			HttpServer3RW.this.addPage(page);
		});
	}

	/**
	 * Allows a page to be processed then returning a Page from a separate thread.
	 * 
	 * @since 3.0
	 * @param page - Returnable&lt;Page&gt;
	 */
	public void deferredAddPage(final Returnable<Page> page) {
		td.asyncSubmit(() -> {
			HttpServer3RW.this.addPage(page.run());
		});
	}

	@Override
	public boolean addPage(final Page page) {
		if (page == null) {
			return false;
		}
		if (page instanceof HttpPage) {
			if (this.flags.use_convert_function)
				return this.PAGES.put(((HttpPage) page).getID(), HttpPage2.convert((HttpPage) page)) == null;
			else
				return this.PAGES.put(((HttpPage) page).getID(), page) == null;
		} else if (page instanceof HttpPage2) {
			return this.PAGES.put(((HttpPage2) page).getID(), page) == null;
		} else if (page instanceof HttpPage3) {
			if (((HttpPage3) page).isCaching() && this.cache != null) {
				final CachedPage c = this.cache.cache(((HttpPage3) page).getID(), page);
				if (c != null) {
					return this.PAGES.put(c.getID(), page) == null;
				} else {
					return this.PAGES.put(((HttpPage3) page).getID(), page) == null;
				}
			} else {
				return this.PAGES.put(((HttpPage3) page).getID(), page) == null;
			}
		} else if (page instanceof IWebSocketPage && this.flags.enable_ws) {
			return this.PAGES.put(((IWebSocketPage) page).getID(), page) == null;
		} else if (page instanceof SSEPage && this.flags.enable_sse) {
			return this.PAGES.put(((SSEPage) page).getID(), page) == null;
		} else {
			log.warn("Unknown page type, ignored. [page: " + page + "]");
			return false;
		}
	}

	public void addFileString(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(Request request) {
				return new Response(200, createStream(file));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileString] Failed to add: " + name);
		}
		;
	}

	public void addFileStringWithMime(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, createStream(file));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileStringWithMime] Failed to add: " + name);
		}
		;
	}

	public void addFileBytes(final String name, final byte[] data) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(data, "data");
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(final Request request) {
				return new Response(200, createStream(data));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileBytes] Failed to add: " + name);
		}
		;
	}

	public void addFileBytesWithMime(final String name, final byte[] data) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(data, "data");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3("/" + name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, createStream(data));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addFileBytesWithMime] Failed to add: " + name);
		}
		;
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName) {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(assetName, "assetName");
		Variables.requireNonNull(reference, "reference");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, FileUtil.getResourceStream(reference, assetName));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addDynamicAssetBytes] Failed to add: " + name);
		}
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName, final CachingMethod cache) {
		this.addDynamicAssetBytes(name, reference, assetName, cache, false);
	}

	public void addDynamicAssetBytes(final String name, final Class<?> reference, final String assetName, final CachingMethod cache,
			final boolean captchaExclusion) {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(assetName, "assetName");
		Variables.requireNonNull(reference, "reference");
		Variables.requireNonNull(cache, "cache");
		final MimeType mt = MimeType.fromFileString(name);
		if (!this.addPage(new HttpPage3(name) {

			@Override
			public Response onRequest(final Request request) {
				final Response r = new Response(200, FileUtil.getResourceStream(reference, assetName));
				if (mt != MimeType.UNKNOWN)
					r.setCustomMime(mt.getType());
				return r;
			}
		}.setCachingMethod(cache).setCaptchaExclusion(captchaExclusion).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA,
				EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS))) {
			log.error("[addDynamicAssetBytes] Failed to add: " + name);
		}
	}

	public void overrideFileString(final String name, final String file) throws Exception {
		Variables.requireNonNull(name, "name");
		Variables.requireNonNull(file, "file");
		final HttpPage3 page = new HttpPage3("/" + name) {

			@Override
			public Response onRequest(Request request) {
				return new Response(200, createStream(file));
			}
		}.setCachingMethod(CachingMethod.SHORT_CACHE).setChecks(EnhancedSecurityChecks.DISALLOW_POST_DATA, EnhancedSecurityChecks.ONLY_ALLOW_BASIC_METHODS);
		this.PAGES.put(page.getID(), page);
	}

	public HttpServer3Settings settings() {
		return this.settings;
	}

	public HttpServer3Flags flags() {
		return this.flags;
	}

	public Logger logger() {
		return log;
	}

	public static void main(String[] args) throws Exception {
		Arguments.setup(args);
		final HttpServer3RW hs3 = new HttpServer3RW(9000, true, SSLBehaviour.DISABLED, null);
		hs3.isTimingDebugEnabled = true;
		log.debugEnabled.set(true);
		hs3.flags.enable_http2 = true;
		hs3.addPage(new CDNCGITracePage());
		hs3.addPage(new TCPDebugPage());
		hs3.addPage(new ASyncHttpPage3("/") {

			@SuppressWarnings("resource")
			@Override
			public void onFutureRequest(final Request request, final ControllableFutureTask<Response> r) {
				try {
					if (request.hasData()) {
						JVM.println("data: " + new String(IOUtils.toByteArray(request.getInputStream())));
					}
					r.complete(new Response(200, createStream("<html><body>" + "\n"
							+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum gravida congue risus, gravida pretium sapien hendrerit nec. Nullam mattis ipsum vitae purus consequat rhoncus. Fusce nisi arcu, scelerisque ac eros non, cursus rhoncus ipsum. Morbi pharetra commodo nisl non bibendum. Quisque ac magna odio. Aliquam vel mattis lacus. Nam ornare, tortor ut hendrerit placerat, turpis metus tempus est, id rhoncus massa velit a nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;\n"
							+ "\n"
							+ "Nulla egestas, odio eget tincidunt maximus, ante elit condimentum tellus, nec eleifend nunc massa ac nisl. Ut iaculis sodales vehicula. Nam ipsum sem, finibus eget bibendum at, dictum vel lorem. Integer blandit leo mauris, et vehicula leo rutrum a. Nam congue nisi lacus, nec commodo justo ornare sed. Pellentesque aliquet sem ut finibus interdum. Donec porttitor rhoncus lectus, vel iaculis enim posuere ut. Sed quis pretium ante, quis semper sapien. Fusce in est tellus. Quisque at libero ut sem efficitur rutrum. In hac habitasse platea dictumst. Praesent mollis aliquam erat a tincidunt.\n"
							+ "\n"
							+ "Mauris neque libero, lacinia at sodales nec, maximus a ligula. Sed sodales tellus ac eros posuere, at malesuada nulla venenatis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque leo a sapien fringilla iaculis. Nunc molestie, augue posuere feugiat accumsan, enim libero semper sem, a finibus mi purus nec orci. Phasellus finibus consequat odio, id condimentum magna pulvinar sed. Sed a aliquam nunc. Etiam convallis sem in ante sagittis auctor. Nullam fringilla ante nisi, at volutpat arcu accumsan sed. Interdum et malesuada fames ac ante ipsum primis in faucibus.\n"
							+ "\n"
							+ "Nullam orci dolor, sollicitudin sit amet placerat at, iaculis non augue. Nam at laoreet velit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque vel metus eu ipsum placerat sagittis. Proin massa dui, lobortis ac nisl ut, lobortis gravida tellus. In non facilisis nibh. Vivamus sit amet luctus lacus, vel molestie urna. Etiam fringilla, enim nec lacinia cursus, velit lectus faucibus massa, sit amet egestas tortor lacus auctor massa. Morbi vitae venenatis nibh, at condimentum velit.\n"
							+ "\n"
							+ "Cras vulputate, sapien id varius faucibus, justo purus aliquam dui, sed hendrerit leo purus eget risus. Ut porttitor tristique tellus, ac blandit tellus. Nullam accumsan ornare aliquam. Maecenas tincidunt sem nec viverra facilisis. Nullam egestas lacus sit amet velit condimentum rhoncus. Sed ac ornare tortor. Phasellus tincidunt eu lorem vitae varius. Proin euismod rutrum facilisis. Aliquam eu feugiat neque. Fusce varius felis non magna feugiat vehicula sed quis arcu. Ut ac enim quam. Donec porta luctus mauris eget interdum. Nullam sit amet pellentesque enim, pharetra varius eros. Aliquam fringilla volutpat libero, vitae ornare purus condimentum vel. Integer a odio risus.</body></html>"))
							.header("X-Test", "Hello").setCustomMime("text/html"));
				} catch (Exception e) {
					r.complete(new Response(503));
				}
			}

		}.setCachingMethod(CachingMethod.VERY_LONG_CACHE));
		hs3.addPage(new ASyncHttpPage3("/proxy-demo") {

			@Override
			public void onFutureRequest(final Request request, final ControllableFutureTask<Response> r) {
				try {
					if (request.hasData()) {
						JVM.println("data: " + new String(IOUtils.toByteArray(request.getInputStream())));
					}
					final HttpResponse<InputStream> rx = HttpClient.getStream().url("http://motherfuckingwebsite.com/").create();
					r.complete(new Response(200, rx, rx));
				} catch (Exception e) {
					r.complete(new Response(503));
				}
			}

		}.setCachingMethod(CachingMethod.VERY_LONG_CACHE));
	}

	public MicroCache getCache() {
		return this.cache;
	}

	public void setDebugging(final boolean value) {
		this.isTimingDebugEnabled = value;
		log.debugEnabled.set(value);
		log.traceEnabled.set(value);
	}

	protected ExecutorService getExecutor() {
		return this.td.executor();
	}

	public void dumpPages() {
		ArrayUtils.printLln(this.PAGES);
	}
}
